feat(api): add credit purchase idempotency key - #4592
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (9)
📒 Files selected for processing (15)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (14)
📝 WalkthroughWalkthroughAdds an optional idempotency ChangesCredit Grant Idempotency Key
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds idempotency support for credit grant creation. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (5): Last reviewed commit: "fix(api): add idempotency key to create ..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api/spec/packages/aip/src/customers/credits/grant.tsp`:
- Around line 196-205: The shared CreditGrant model is exposing the create-only
idempotency_key in read-side generated contracts, so move this field off the
reusable CreditGrant shape and into a request-only create model or equivalent
create input used by the grant operation. Update the TypeSpec around the grant
creation model so the shared response/read model stays free of idempotency_key
while the create request still accepts it, keeping the generated schemas/types
in sync with the intended contract.
In `@openmeter/billing/creditgrant/service.go`:
- Around line 60-68: `CreateInput.Validate()` needs to enforce the same
`IdempotencyKey` length limit as the API/DB contract. Update the validation
logic in `CreateInput.Validate` to add an error when `IdempotencyKey` is present
and longer than 255 characters, using the existing `var errs []error` pattern
and returning `models.NewNillableGenericValidationError(errors.Join(errs...))`
rather than failing fast. Make sure the check is applied alongside the other
field validations in `CreateInput`.
In `@openmeter/ent/schema/chargescreditpurchase.go`:
- Around line 67-70: The idempotency key field in the ent schema is currently
unbounded, so update the `field.String("idempotency_key")` definition in
`chargescreditpurchase.go` to enforce the 255-character limit by adding
`MaxLen(255)` alongside the existing optional/nillable/immutable settings. Keep
the change in the `ent/schema` source of truth so the generated column uses the
same DB contract as the public API.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 126094f1-79b4-4864-b558-439f795542f6
⛔ Files ignored due to path filters (9)
api/v3/openapi.yamlis excluded by!**/openapi.yamlopenmeter/ent/db/chargecreditpurchase.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase/where.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase_create.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase_update.gois excluded by!**/ent/db/**openmeter/ent/db/migrate/schema.gois excluded by!**/ent/db/**openmeter/ent/db/mutation.gois excluded by!**/ent/db/**tools/migrate/migrations/atlas.sumis excluded by!**/*.sum,!**/*.sum
📒 Files selected for processing (16)
api/spec/packages/aip-client-javascript/src/models/schemas.tsapi/spec/packages/aip-client-javascript/src/models/types.tsapi/spec/packages/aip/src/customers/credits/grant.tspapi/v3/api.gen.goapi/v3/handlers/customers/credits/convert.goe2e/customer_credits_v3_test.goe2e/v3helpers_test.goopenmeter/billing/charges/creditpurchase/adapter/charge.goopenmeter/billing/charges/creditpurchase/adapter/mapper.goopenmeter/billing/charges/creditpurchase/charge.goopenmeter/billing/creditgrant/service.goopenmeter/billing/creditgrant/service/service.goopenmeter/ent/schema/chargescreditpurchase.gotest/credits/creditgrant_test.gotools/migrate/migrations/20260628214302_credit-purchase-idempotency-key.down.sqltools/migrate/migrations/20260628214302_credit-purchase-idempotency-key.up.sql
💤 Files with no reviewable changes (2)
- test/credits/creditgrant_test.go
- e2e/v3helpers_test.go
| Currency currencyx.Code | ||
| Amount alpacadecimal.Decimal | ||
| Priority *int16 | ||
| FundingMethod FundingMethod | ||
| Purchase *PurchaseTerms | ||
| TaxConfig *productcatalog.TaxConfig | ||
| Filters *GrantFilters | ||
| ExpiresAfter *datetime.ISODuration | ||
| IdempotencyKey *string |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Validate IdempotencyKey in CreateInput.Validate() too.
The new field enters the domain here, but Validate() still accepts arbitrary-length values. That lets non-HTTP callers bypass the 255-character limit and drift from the API/DB contract. Please append a validation error for keys longer than 255 characters.
Suggested fix
+import "unicode/utf8"
+
func (i CreateInput) Validate() error {
var errs []error
@@
if i.Filters != nil {
if err := creditpurchase.FeatureFilters(i.Filters.Features).Validate(); err != nil {
errs = append(errs, fmt.Errorf("filters.features: %w", err))
}
}
+
+ if i.IdempotencyKey != nil && utf8.RuneCountInString(*i.IdempotencyKey) > 255 {
+ errs = append(errs, errors.New("idempotency_key must be at most 255 characters"))
+ }
return models.NewNillableGenericValidationError(errors.Join(errs...))
}As per coding guidelines, For Go Validate() error methods, collect all validation issues into var errs []error and return models.NewNillableGenericValidationError(errors.Join(errs...)) instead of failing fast.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@openmeter/billing/creditgrant/service.go` around lines 60 - 68,
`CreateInput.Validate()` needs to enforce the same `IdempotencyKey` length limit
as the API/DB contract. Update the validation logic in `CreateInput.Validate` to
add an error when `IdempotencyKey` is present and longer than 255 characters,
using the existing `var errs []error` pattern and returning
`models.NewNillableGenericValidationError(errors.Join(errs...))` rather than
failing fast. Make sure the check is applied alongside the other field
validations in `CreateInput`.
Source: Coding guidelines
7e7e1fc to
d66eca1
Compare
| /** | ||
| * Idempotency key for the credit grant creation request. | ||
| * | ||
| * When provided, reusing the same key returns an HTTP 409 Conflict | ||
| * instead of creating a duplicate grant, which makes create requests safe to retry. | ||
| */ | ||
| key?: string |
There was a problem hiding this comment.
Response Type Leaks Key
The server treats key as create-only, but the generated TypeScript response model still exposes it on CreditGrant. The OpenAPI response schema and Go response conversion do not serialize this field, so TS callers of create/get/list can read CreditGrant.key from the SDK type and always receive undefined at runtime. Please keep the field on CreateCreditGrantRequest only, or update the response contract and serializer to return it consistently.
Context Used: api/spec/AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: api/spec/packages/aip-client-javascript/src/models/types.ts
Line: 3649-3655
Comment:
**Response Type Leaks Key**
The server treats `key` as create-only, but the generated TypeScript response model still exposes it on `CreditGrant`. The OpenAPI response schema and Go response conversion do not serialize this field, so TS callers of create/get/list can read `CreditGrant.key` from the SDK type and always receive `undefined` at runtime. Please keep the field on `CreateCreditGrantRequest` only, or update the response contract and serializer to return it consistently.
**Context Used:** api/spec/AGENTS.md ([source](https://app.greptile.com/openmeter/github/openmeterio/openmeter/-/custom-context?memory=28ba6068-00f9-4629-9b78-8e49cc802858))
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api/spec/packages/aip/src/customers/credits/grant.tsp`:
- Around line 196-200: The documentation for the credit grant idempotency key in
grant.tsp currently implies any key reuse always returns 409, which is broader
than the actual behavior. Update the comment near the credit grant request
definition to state the idempotency scope accurately: duplicate reuse should
conflict only within the same customer/context, while cross-customer reuse is
allowed. Keep the wording aligned with the request/response contract described
by the grant creation model so the docs match the implementation.
In `@openmeter/ent/schema/chargescreditpurchase.go`:
- Around line 123-130: The uniqueness constraint in the charges credit purchase
schema is too broad because `index.Fields("namespace", "key")` makes `key`
unique across an entire namespace instead of per customer. Update the unique
index in `chargescreditpurchase.go` to include `customer_id` (or the intended
ownership boundary) alongside `namespace` and `key`, keeping the partial
`deleted_at IS NULL` condition. Then regenerate the migration so the database
schema matches the updated uniqueness scope.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ce6d975b-7b1f-41b3-9ceb-4849ac072384
⛔ Files ignored due to path filters (9)
api/v3/openapi.yamlis excluded by!**/openapi.yamlopenmeter/ent/db/chargecreditpurchase.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase/where.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase_create.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase_update.gois excluded by!**/ent/db/**openmeter/ent/db/migrate/schema.gois excluded by!**/ent/db/**openmeter/ent/db/mutation.gois excluded by!**/ent/db/**tools/migrate/migrations/atlas.sumis excluded by!**/*.sum,!**/*.sum
📒 Files selected for processing (15)
api/spec/packages/aip-client-javascript/src/models/schemas.tsapi/spec/packages/aip-client-javascript/src/models/types.tsapi/spec/packages/aip/src/customers/credits/grant.tspapi/v3/api.gen.goapi/v3/handlers/customers/credits/convert.goe2e/customer_credits_v3_test.goopenmeter/billing/charges/creditpurchase/adapter/charge.goopenmeter/billing/charges/creditpurchase/adapter/mapper.goopenmeter/billing/charges/creditpurchase/charge.goopenmeter/billing/creditgrant/service.goopenmeter/billing/creditgrant/service/service.goopenmeter/ent/schema/chargescreditpurchase.gotest/credits/creditgrant_test.gotools/migrate/migrations/20260629111024_credit-purchase-idempotency-key.down.sqltools/migrate/migrations/20260629111024_credit-purchase-idempotency-key.up.sql
💤 Files with no reviewable changes (1)
- test/credits/creditgrant_test.go
✅ Files skipped from review due to trivial changes (1)
- api/v3/api.gen.go
🚧 Files skipped from review as they are similar to previous changes (3)
- openmeter/billing/charges/creditpurchase/charge.go
- openmeter/billing/creditgrant/service/service.go
- openmeter/billing/charges/creditpurchase/adapter/charge.go
| /** | ||
| * Idempotency key for the credit grant creation request. | ||
| * | ||
| * When provided, reusing the same key returns an HTTP 409 Conflict instead of | ||
| * creating a duplicate grant, which makes create requests safe to retry. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clarify the idempotency scope in the docs.
The wording reads like any key reuse returns 409, but the supplied stack context says cross-customer reuse is allowed. Tightening this to the actual scope would keep the contract honest.
✏️ Suggested wording
- * When provided, reusing the same key returns an HTTP 409 Conflict instead of
- * creating a duplicate grant, which makes create requests safe to retry.
+ * When provided, reusing the same key for the same customer returns an HTTP 409
+ * Conflict instead of creating a duplicate grant, which makes create requests
+ * safe to retry.As per path instructions, "The declared API should be accurate, in parity with the actual implementation, and easy to understand."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Idempotency key for the credit grant creation request. | |
| * | |
| * When provided, reusing the same key returns an HTTP 409 Conflict instead of | |
| * creating a duplicate grant, which makes create requests safe to retry. | |
| /** | |
| * Idempotency key for the credit grant creation request. | |
| * | |
| * When provided, reusing the same key for the same customer returns an HTTP 409 | |
| * Conflict instead of creating a duplicate grant, which makes create requests | |
| * safe to retry. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@api/spec/packages/aip/src/customers/credits/grant.tsp` around lines 196 -
200, The documentation for the credit grant idempotency key in grant.tsp
currently implies any key reuse always returns 409, which is broader than the
actual behavior. Update the comment near the credit grant request definition to
state the idempotency scope accurately: duplicate reuse should conflict only
within the same customer/context, while cross-customer reuse is allowed. Keep
the wording aligned with the request/response contract described by the grant
creation model so the docs match the implementation.
Source: Path instructions
aacb385 to
d68eff6
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@tools/migrate/migrations/20260629113257_credit-purchase-idempotency-key.up.sql`:
- Line 5: The unique index on charge_credit_purchases is scoped too broadly
because it only covers namespace and key, which blocks the intended
cross-customer reuse behavior. Update the migration’s CREATE UNIQUE INDEX for
charge_credit_purchases to include the customer discriminator used by this
table, alongside namespace and key, while preserving the deleted_at filter and
non-null key condition. Adjust the index definition so the uniqueness constraint
matches the API’s customer-level idempotency scope.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 739d8768-3f84-4954-9bba-f458d985f6b1
⛔ Files ignored due to path filters (9)
api/v3/openapi.yamlis excluded by!**/openapi.yamlopenmeter/ent/db/chargecreditpurchase.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase/where.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase_create.gois excluded by!**/ent/db/**openmeter/ent/db/chargecreditpurchase_update.gois excluded by!**/ent/db/**openmeter/ent/db/migrate/schema.gois excluded by!**/ent/db/**openmeter/ent/db/mutation.gois excluded by!**/ent/db/**tools/migrate/migrations/atlas.sumis excluded by!**/*.sum,!**/*.sum
📒 Files selected for processing (15)
api/spec/packages/aip-client-javascript/src/models/schemas.tsapi/spec/packages/aip-client-javascript/src/models/types.tsapi/spec/packages/aip/src/customers/credits/grant.tspapi/v3/api.gen.goapi/v3/handlers/customers/credits/convert.goe2e/customer_credits_v3_test.goopenmeter/billing/charges/creditpurchase/adapter/charge.goopenmeter/billing/charges/creditpurchase/adapter/mapper.goopenmeter/billing/charges/creditpurchase/charge.goopenmeter/billing/creditgrant/service.goopenmeter/billing/creditgrant/service/service.goopenmeter/ent/schema/chargescreditpurchase.gotest/credits/creditgrant_test.gotools/migrate/migrations/20260629113257_credit-purchase-idempotency-key.down.sqltools/migrate/migrations/20260629113257_credit-purchase-idempotency-key.up.sql
💤 Files with no reviewable changes (1)
- test/credits/creditgrant_test.go
✅ Files skipped from review due to trivial changes (2)
- openmeter/billing/creditgrant/service/service.go
- openmeter/billing/charges/creditpurchase/charge.go
🚧 Files skipped from review as they are similar to previous changes (10)
- openmeter/billing/charges/creditpurchase/adapter/mapper.go
- api/v3/handlers/customers/credits/convert.go
- api/spec/packages/aip/src/customers/credits/grant.tsp
- openmeter/billing/charges/creditpurchase/adapter/charge.go
- api/spec/packages/aip-client-javascript/src/models/schemas.ts
- api/v3/api.gen.go
- openmeter/billing/creditgrant/service.go
- e2e/customer_credits_v3_test.go
- openmeter/ent/schema/chargescreditpurchase.go
- api/spec/packages/aip-client-javascript/src/models/types.ts
| ALTER TABLE "charge_credit_purchases" ADD COLUMN "key" character varying NULL; | ||
| -- create index "chargecreditpurchase_namespace_key" to table: "charge_credit_purchases" | ||
| -- atlas:nolint MF101 | ||
| CREATE UNIQUE INDEX "chargecreditpurchase_namespace_key" ON "charge_credit_purchases" ("namespace", "key") WHERE ((key IS NOT NULL) AND (deleted_at IS NULL)); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Scope the unique index to the customer, not just the namespace.
Right now this makes key unique across every credit purchase in a namespace. That would also reject the “cross-customer reuse” case called out in the stack context, so the DB constraint looks broader than the intended API behavior. Please include the customer discriminator used by charge_credit_purchases in this index as well.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@tools/migrate/migrations/20260629113257_credit-purchase-idempotency-key.up.sql`
at line 5, The unique index on charge_credit_purchases is scoped too broadly
because it only covers namespace and key, which blocks the intended
cross-customer reuse behavior. Update the migration’s CREATE UNIQUE INDEX for
charge_credit_purchases to include the customer discriminator used by this
table, alongside namespace and key, while preserving the deleted_at filter and
non-null key condition. Adjust the index definition so the uniqueness constraint
matches the API’s customer-level idempotency scope.
d68eff6 to
2d75c96
Compare
Summary by CodeRabbit
New Features
Bug Fixes
Tests